﻿/*
	Copyright (c) 2013 Andrew Meyer

	Permission is hereby granted, free of charge, to any person obtaining a copy of this software
	and associated documentation files (the "Software"), to deal in the Software without
	restriction, including without limitation the right to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all copies or
	substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
	BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
	DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace Ascend
{
	/// <summary>
	/// Control used for viewing an Ascend.Scene3D.
	/// </summary>
	public partial class SceneViewer3D : UserControl
	{
		#region Lifecycle
		/// <summary>
		/// Initializes a new instance of the Ascend.SceneViewer3D class.
		/// </summary>
		public SceneViewer3D()
		{
			InitializeComponent();

			MouseDoubleClick += SceneViewer3D_MouseDoubleClick;
			MouseDown += SceneViewer3D_MouseDown;
			MouseUp += SceneViewer3D_MouseUp;
			MouseMove += SceneViewer3D_MouseMove;
			MouseWheel += SceneViewer3D_MouseWheel;
		}
		#endregion

		#region Private Members
		private bool _leftMouseDown = false;
		private bool _rightMouseDown = false;
		private bool _midMouseDown = false;
		private bool _midKeyDown = false;
		private bool _rightKeyDown = false;
		private Point _mouseReferencePosition;
		private Point3D _cameraReferencePosition;
		double _horizontalAngle;
		double _verticalAngle;
		ModelSceneNode _lastEnteredNode;
		#endregion

		#region Dependency Properties
		/// <summary>
		/// Gets or sets the Ascend.Scene3D to view through this control.
		/// </summary>
		public Scene3D Scene
		{
			get
			{
				return (Scene3D)GetValue(SceneProperty);
			}
			
			set
			{
				SetValue(SceneProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.Scene dependency property.
		/// </summary>
		public static readonly DependencyProperty SceneProperty =
			DependencyProperty.Register("Scene", typeof(Scene3D), typeof(SceneViewer3D));

		/// <summary>
		/// Gets or sets whether the Ascend.Scene3D's camera can be controlled with the mouse.
		/// </summary>
		public bool MouseCameraControl
		{
			get
			{
				return (bool)GetValue(MouseCameraControlProperty);
			}
			
			set
			{
				SetValue(MouseCameraControlProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.MouseCameraControl dependency property.
		/// </summary>
		public static readonly DependencyProperty MouseCameraControlProperty =
			DependencyProperty.Register("MouseCameraControl", typeof(bool), typeof(SceneViewer3D),
				new PropertyMetadata(false));

		/// <summary>
		/// Gets or sets whether the keyboard can be used to emulate middle and right mouse buttons,
		/// which is convenient for trackpads.
		/// </summary>
		public bool KeyboardMouseEmulation
		{
			get
			{
				return (bool)GetValue(KeyboardMouseEmulationProperty);
			}
			
			set
			{
				SetValue(KeyboardMouseEmulationProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.KeyboardMouseEmulation dependency property.
		/// </summary>
		public static readonly DependencyProperty KeyboardMouseEmulationProperty =
			DependencyProperty.Register("KeyboardMouseEmulation", typeof(bool),
				typeof(SceneViewer3D), new PropertyMetadata(false));


		/// <summary>
		/// Gets or sets key that must be held during a left-click in order to emulate a
		/// right-click.
		/// </summary>
		public Key MiddleMouseEmulatorKey
		{
			get
			{
				return (Key)GetValue(MiddleMouseEmulatorKeyProperty);
			}
			
			set
			{
				SetValue(MiddleMouseEmulatorKeyProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.MiddleMouseEmulatorKey dependency property.
		/// </summary>
		public static readonly DependencyProperty MiddleMouseEmulatorKeyProperty =
			DependencyProperty.Register("MiddleMouseEmulatorKey", typeof(Key),
				typeof(SceneViewer3D), new PropertyMetadata(Key.LeftAlt));


		/// <summary>
		/// Gets or sets key that must be held during a left-click in order to emulate a
		/// middle-click.
		/// </summary>
		public Key RightMouseEmulatorKey
		{
			get
			{
				return (Key)GetValue(RightMouseEmulatorKeyProperty);
			}
			
			set
			{
				SetValue(RightMouseEmulatorKeyProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.RightMouseEmulator dependency property.
		/// </summary>
		public static readonly DependencyProperty RightMouseEmulatorKeyProperty =
			DependencyProperty.Register("RightMouseEmulatorKey", typeof(Key), typeof(SceneViewer3D),
				new PropertyMetadata(Key.LeftCtrl));


		/// <summary>
		/// Gets or sets the camera pan sensitivity when controlling the camera with a mouse.
		/// </summary>
		public double CameraPanSensitivity
		{
			get
			{
				return (double)GetValue(CameraPanSensitivityProperty);
			}
			
			set
			{
				SetValue(CameraPanSensitivityProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.CameraPanSensitivity dependency property.
		/// </summary>
		public static readonly DependencyProperty CameraPanSensitivityProperty =
			DependencyProperty.Register("CameraPanSensitivity", typeof(double),
				typeof(SceneViewer3D), new PropertyMetadata(1.0));


		/// <summary>
		/// Gets or sets the camera zoom sensitivity when controlling the camera with a mouse.
		/// </summary>
		public double CameraZoomSensitivity
		{
			get
			{
				return (double)GetValue(CameraZoomSensitivityProperty);
			}
			
			set
			{
				SetValue(CameraZoomSensitivityProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.CameraZoomSensitivity dependency property.
		/// </summary>
		public static readonly DependencyProperty CameraZoomSensitivityProperty =
			DependencyProperty.Register("CameraZoomSensitivity", typeof(double),
				typeof(SceneViewer3D), new PropertyMetadata(1.0));


		/// <summary>
		/// Gets or sets the camera rotation sensitivity when controlling the camera with a mouse.
		/// </summary>
		public double CameraRotationSensitivity
		{
			get
			{
				return (double)GetValue(CameraRotationSensitivityProperty);
			}
			
			set
			{
				SetValue(CameraRotationSensitivityProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.SceneViewer3D.CameraRotationSensitivity dependency property.
		/// </summary>
		public static readonly DependencyProperty CameraRotationSensitivityProperty =
			DependencyProperty.Register("CameraRotationSensitivity", typeof(double),
				typeof(SceneViewer3D), new PropertyMetadata(1.0));
		#endregion

		#region Private Methods
		private void PanCamera(MouseEventArgs e)
		{
			ProjectionCamera cam = Scene.Camera;

			// Get camera vectors
			Vector3D right = new Vector3D(Math.Sin(_horizontalAngle + Math.PI / 2), 0,
				Math.Cos(_horizontalAngle + Math.PI / 2));
			Vector3D look = cam.LookDirection;
			Vector3D up = Vector3D.CrossProduct(right, look);
			Vector3D forw = new Vector3D(look.X, 0, look.Z);
			
			right.Normalize();
			up.Normalize();
			forw.Normalize();

			// Update camera position based on mouse movement
			Vector mouseDelta = (_mouseReferencePosition - e.GetPosition(this));

			double dist = look.Length;
			double scale = 0.002 * CameraPanSensitivity *
				((cam is OrthographicCamera)
					? 0.9 * ((OrthographicCamera)cam).Width
					: dist);
			double dr = mouseDelta.X * scale;
			double du = Convert.ToByte(_rightMouseDown) * mouseDelta.Y * -scale;
			double df = Convert.ToByte(_midMouseDown) * mouseDelta.Y * -scale;
			cam.Position = _cameraReferencePosition + (dr * right) + (du * up) + (df * forw);
		}

		private void RotateCamera(MouseEventArgs e)
		{
			ProjectionCamera cam = Scene.Camera;

			// Get camera's target point
			Point3D target = cam.Position + Scene.Camera.LookDirection;

			// Get camera angles based on mouse movement
			Vector mouseDelta = (_mouseReferencePosition - e.GetPosition(this)) *
				CameraRotationSensitivity;
			double angleH = _horizontalAngle + mouseDelta.X * 0.02;
			double angleV = _verticalAngle + mouseDelta.Y * -0.02;

			// Update camera position
			double rho = cam.LookDirection.Length;
			double r = rho * Math.Cos(angleV);
			double x = r * Math.Sin(angleH);
			double z = r * Math.Cos(angleH);
			double y = rho * Math.Sin(angleV);

			cam.Position = (Vector3D)target + new Point3D(x, y, z);

			// Update camera look direction
			cam.LookDirection = target - cam.Position;

			// Update camera up direction
			Vector3D right = new Vector3D(Math.Sin(angleH + Math.PI / 2), 0,
				Math.Cos(angleH + Math.PI / 2));
			Vector3D look = cam.LookDirection;
			cam.UpDirection = Vector3D.CrossProduct(right, look);
		}

		private void ReleaseMouseButtons()
		{
			_leftMouseDown = false;
			_rightMouseDown = false;
			_midMouseDown = false;
			_midKeyDown = false;
			_rightKeyDown = false;

			ReleaseMouseCapture();
		}

		private void ComputeCameraAngles()
		{
			ProjectionCamera cam = Scene.Camera;

			// Get position relative to target
			double dx = -cam.LookDirection.X;
			double dy = -cam.LookDirection.Y;
			double dz = -cam.LookDirection.Z;

			double rho = cam.LookDirection.Length;

			_verticalAngle = Math.Asin(dy / rho);	// Angle from X-Z plane
			_horizontalAngle = Math.Atan2(dx, dz);  // Angle from Z-Y plane

			// Compensate for upside-down camera
			if (Scene.Camera.UpDirection.Y < 0)
			{
				_verticalAngle = Math.PI - _verticalAngle;
				_horizontalAngle += Math.PI;
			}
		}

		private void CheckSceneNodeMouseButtonEvent(MouseButtonEventArgs e,
			Action<SceneNode, SceneNodeMouseButtonEventArgs> eventRaiseMethod,
			Action<SceneNode, SceneNodeMouseButtonEventArgs> previewEventRaiseMethod)
		{
			RayHitTestResult result;
			ModelSceneNode hitNode = GetHitSceneNode(e.GetPosition(this), out result);

			if (hitNode != null)
			{
				RouteSceneNodeMouseButtonEvent(hitNode, result, e, eventRaiseMethod,
					previewEventRaiseMethod);
			}
		}

		private void RouteSceneNodeMouseButtonEvent(ModelSceneNode sourceNode,
			RayHitTestResult result, MouseButtonEventArgs e,
			Action<SceneNode, SceneNodeMouseButtonEventArgs> eventRaiseMethod,
			Action<SceneNode, SceneNodeMouseButtonEventArgs> previewEventRaiseMethod)
		{
			SceneNodeMouseButtonEventArgs args =
				new SceneNodeMouseButtonEventArgs(e, this, sourceNode, result);

			// Get bubbling and tunneling event routes
			List<SceneNode> bubblingEventRoute = GetBubblingEventRoute(sourceNode);
			List<SceneNode> tunnelingEventRoute = bubblingEventRoute.ToList();
			tunnelingEventRoute.Reverse();

			// Invoke tunneling events first
			foreach (SceneNode node in tunnelingEventRoute)
			{
				previewEventRaiseMethod(node, args);

				if (args.Handled)
				{
					return;
				}
			}

			// Next, invoke bubbling events
			foreach (SceneNode node in bubblingEventRoute)
			{
				eventRaiseMethod(node, args);

				if (args.Handled)
				{
					return;
				}
			}
		}

		private void CheckSceneNodeMouseMoveEvent(MouseEventArgs e)
		{
			RayHitTestResult result;
			ModelSceneNode hitNode = GetHitSceneNode(e.GetPosition(this), out result);

			if (hitNode != null)
			{
				if (_lastEnteredNode != hitNode)
				{
					if (_lastEnteredNode != null)
					{
						_lastEnteredNode.SetMouseOverState(false,
							new SceneNodeMouseEventArgs(e, this, _lastEnteredNode, result));
					}

					_lastEnteredNode = hitNode;

					hitNode.SetMouseOverState(true,
						new SceneNodeMouseEventArgs(e, this, hitNode, result));
				}

				RouteSceneNodeMouseMoveEvent(hitNode, result, e);
			}
			else if (_lastEnteredNode != null)
			{
				_lastEnteredNode.SetMouseOverState(false,
					new SceneNodeMouseEventArgs(e, this, _lastEnteredNode, result));

				_lastEnteredNode = null;
			}
		}

		private void RouteSceneNodeMouseMoveEvent(ModelSceneNode sourceNode,
			RayHitTestResult result, MouseEventArgs e)
		{
			SceneNodeMouseEventArgs args = new SceneNodeMouseEventArgs(e, this, sourceNode, result);

			// Get bubbling and tunneling event routes
			List<SceneNode> bubblingEventRoute = GetBubblingEventRoute(sourceNode);
			List<SceneNode> tunnelingEventRoute = bubblingEventRoute.ToList();
			tunnelingEventRoute.Reverse();

			// Invoke tunneling events first
			foreach (SceneNode node in tunnelingEventRoute)
			{
				SceneNode.RaisePreviewMouseMoveEvent(node, args);

				if (args.Handled)
				{
					return;
				}
			}

			// Next, invoke bubbling events
			foreach (SceneNode node in bubblingEventRoute)
			{
				SceneNode.RaiseMouseMoveEvent(node, args);

				if (args.Handled)
				{
					return;
				}
			}
		}

		private void CheckSceneNodeMouseWheelEvent(MouseWheelEventArgs e)
		{
			RayHitTestResult result;
			ModelSceneNode hitNode = GetHitSceneNode(e.GetPosition(this), out result);

			if (hitNode != null)
			{
				RouteSceneNodeMouseWheelEvent(hitNode, result, e);
			}
		}

		private void RouteSceneNodeMouseWheelEvent(ModelSceneNode sourceNode,
			RayHitTestResult result, MouseWheelEventArgs e)
		{
			SceneNodeMouseWheelEventArgs args =
				new SceneNodeMouseWheelEventArgs(e, this, sourceNode, result);

			// Get bubbling and tunneling event routes
			List<SceneNode> bubblingEventRoute = GetBubblingEventRoute(sourceNode);
			List<SceneNode> tunnelingEventRoute = bubblingEventRoute.ToList();
			tunnelingEventRoute.Reverse();

			// Invoke tunneling events first
			foreach (SceneNode node in tunnelingEventRoute)
			{
				SceneNode.RaisePreviewMouseWheelEvent(node, args);

				if (args.Handled)
				{
					return;
				}
			}

			// Next, invoke bubbling events
			foreach (SceneNode node in bubblingEventRoute)
			{
				SceneNode.RaiseMouseWheelEvent(node, args);

				if (args.Handled)
				{
					return;
				}
			}
		}

		private ModelSceneNode GetHitSceneNode(Point mousePosition, out RayHitTestResult result)
		{
			result = null;

			if (Scene != null && Scene.RenderRoot != null)
			{
				result = VisualTreeHelper.HitTest(this, mousePosition) as RayHitTestResult;

				if (result != null)
				{
					Model3D modelHit = result.ModelHit;

					IEnumerable<TransformableObject> desc = Scene.RenderRoot.GetDescendants();
					
					return desc.OfType<ModelSceneNode>().FirstOrDefault(n => n.Model == modelHit);
				}
			}

			return null;
		}

		private List<SceneNode> GetBubblingEventRoute(SceneNode node)
		{
			List<SceneNode> route = node.GetAncestors().OfType<SceneNode>().ToList();

			route.Insert(0, node);

			return route;
		}
		#endregion

		#region Event Handlers
		private void SceneViewer3D_MouseDoubleClick(object sender, MouseButtonEventArgs e)
		{
			if (Scene == null || Scene.Camera == null)
			{
				return;
			}

			CheckSceneNodeMouseButtonEvent(e, SceneNode.RaiseMouseDoubleClickEvent,
				SceneNode.RaisePreviewMouseDoubleClickEvent);
		}

		private void SceneViewer3D_MouseDown(object sender, MouseButtonEventArgs e)
		{
			if (Scene == null || Scene.Camera == null)
			{
				return;
			}

			CheckSceneNodeMouseButtonEvent(e, SceneNode.RaiseMouseDownEvent,
				SceneNode.RaisePreviewMouseDownEvent);

			if (!MouseCameraControl)
			{
				return;
			}

			_midKeyDown = KeyboardMouseEmulation && Keyboard.IsKeyDown(MiddleMouseEmulatorKey) &&
				!Keyboard.IsKeyDown(RightMouseEmulatorKey);
			_rightKeyDown = KeyboardMouseEmulation && Keyboard.IsKeyDown(RightMouseEmulatorKey) &&
				!Keyboard.IsKeyDown(MiddleMouseEmulatorKey);

			_leftMouseDown = e.ChangedButton == MouseButton.Left && !_midKeyDown && !_rightKeyDown;
			_rightMouseDown = e.ChangedButton == MouseButton.Right ||
				(e.ChangedButton == MouseButton.Left && _rightKeyDown);
			_midMouseDown = e.ChangedButton == MouseButton.Middle ||
				(e.ChangedButton == MouseButton.Left && _midKeyDown);

			_mouseReferencePosition = e.GetPosition(this);
			_cameraReferencePosition = Scene.Camera.Position;

			ComputeCameraAngles();
			CaptureMouse();
		}

		private void SceneViewer3D_MouseUp(object sender, MouseButtonEventArgs e)
		{
			if (Scene == null || Scene.Camera == null)
			{
				return;
			}

			CheckSceneNodeMouseButtonEvent(e, SceneNode.RaiseMouseUpEvent,
				SceneNode.RaisePreviewMouseUpEvent);

			if (!MouseCameraControl)
			{
				return;
			}

			ReleaseMouseButtons();
		}

		private void SceneViewer3D_MouseMove(object sender, MouseEventArgs e)
		{
			if (Scene == null || Scene.Camera == null)
			{
				return;
			}

			CheckSceneNodeMouseMoveEvent(e);

			if (!MouseCameraControl)
			{
				return;
			}

			if ((_midKeyDown && Keyboard.IsKeyUp(MiddleMouseEmulatorKey)) ||
				(_rightKeyDown && Keyboard.IsKeyUp(RightMouseEmulatorKey)))
			{
				ReleaseMouseButtons();

				return;
			}

			if (_rightMouseDown || _midMouseDown)
			{
				PanCamera(e);

				return;
			}

			if (_leftMouseDown)
			{
				RotateCamera(e);

				return;
			}
		}

		private void SceneViewer3D_MouseWheel(object sender, MouseWheelEventArgs e)
		{
			if (Scene == null || Scene.Camera == null)
			{
				return;
			}

			CheckSceneNodeMouseWheelEvent(e);

			if (!MouseCameraControl)
			{
				return;
			}

			ProjectionCamera cam = Scene.Camera;

			// If camera is orthographic, simply resize its window
			if (cam is OrthographicCamera)
			{
				double deltaWidth = e.Delta * ((OrthographicCamera)cam).Width * 0.002 *
					CameraZoomSensitivity;
				((OrthographicCamera)cam).Width -= deltaWidth;

				return;
			}

			// Get look direction and distance
			Vector3D look = cam.LookDirection;
			double dist = look.Length;

			// Get distance adjustment
			double deltaDist = e.Delta * dist * CameraZoomSensitivity * 0.001;

			// Get camera's target
			Point3D target = cam.Position + look;

			// Move camera along look direction, proportional to distance
			look.Normalize();
			cam.Position += look * deltaDist;

			// Update look direction
			cam.LookDirection = target - cam.Position;
		}
		#endregion
	}
}
